package com.nulldev.util.web.nTubeExtensions.services;

import java.io.IOException;
import java.time.Duration;
import java.util.Map;

import com.nulldev.util.JVM.Arguments;
import com.nulldev.util.JVM.JVM;
import com.nulldev.util.VariableAPI.MathUtil.TimeUtil;
import com.nulldev.util.VariableAPI.RandomUtil;
import com.nulldev.util.VariableAPI.StringsUtil;
import com.nulldev.util.concurrency.threadIt.v4.emapi.ExecutorManager;
import com.nulldev.util.concurrency.threadIt.v4.emapi.uft.Executable;
import com.nulldev.util.data.Variables;
import com.nulldev.util.data.Arrays.maps.sem.SelfExpiringHashMap;
import com.nulldev.util.languages.json.uniapi.UniJsonValue;
import com.nulldev.util.logging.Logger;
import com.nulldev.util.logging.LoggerFactory;
import com.nulldev.util.web.HttpClient.HttpClient;
import com.nulldev.util.web.HttpClient.HttpMethod;
import com.nulldev.util.web.HttpClient.HttpResponse;
import com.nulldev.util.web.legacyURI.LegacyURIUtils;
import com.nulldev.util.web.nTubeExtensions.services.TwitchTV.ChannelURLRequest.RequestType;

public class TwitchTV {
	public static final boolean PREFER_HTTPS = Arguments.hasArgument("--TwitchTV.useHTTPS");
	private static final Logger log = LoggerFactory.getLogger();
	private String clientKey = null;

	/*
	 * TODO for 1.2.2: - Add support for information again - Hook up V6's approach -
	 * Find a fucking oauth token
	 */

	/*
	 * 1.2.2: - Added V6 support. 1.2.1: - Removed USE_TTV_REWRITE_PATCHES, this is
	 * now always enabled. - Updated playerVersion. 1.2: - Fixed some stuff. - Added
	 * support for API v5. - APIv5: Add cache for String->Long mappings. 1.1: -
	 * Added USE_TTV_REWRITE_PATCHES. - Added low-latency mode. - Added mobile mode.
	 * 1.0: - Initial release.
	 */
	public static final String VERSION = "1.2.2";
	private static final String playerVersion = "1.2.0";

	private static final Map<String, Long> CHANNEL_ID_MAPPINGS = new SelfExpiringHashMap<String, Long>(TimeUtil.fromMinutes(15));

	private final Executable<String> accquireClientKeyTask = ExecutorManager.make(() -> {
		try {
			String data0;
			HttpResponse<String> _x;
			try {
				_x = HttpClient.getString().url("https://www.twitch.tv/").timeout(Duration.ofSeconds(10)).create();
				data0 = _x.dataThenClose();
			} catch (Exception e1) {
				throw new RuntimeException(e1);
			}
			final String dat = StringsUtil.substringBetweenO(data0, "\"Client-ID\":\"", "\",");
			if (dat == null) {
				JVM.println(data0);
				throw new UnsupportedOperationException("Failed to extract client keys!");
			}
			return dat;
//			final Document doc = JFiveParse.parse(data0);
//			final List<Element> scripts = doc.getElementsByTagName("script");
//			String coreScript = null;
//			for (final Element e : scripts) {
//				if (e.hasAttribute("src")) {
//					final String attr = e.getAttribute("src");
//					if (attr.startsWith("https://static.twitchcdn.net/assets/core-") && attr.endsWith(".js")) {
//						coreScript = attr;
//						break;
//					}
//				}
//			}
//			log.info("coreScript: " + coreScript);
//			if (coreScript == null) {
//				return null;
//			}
//			String data;
//			try {
//				data = HttpClient.getString()
//						.url(coreScript)
//						.timeout(Duration.ofSeconds(10))
//						.create()
//						.dataThenClose();
//			} catch (Exception e1) {
//				throw new RuntimeException(e1);
//			}
//			final String apiKey = StringsUtil.substringBetweenO(data, "{clientID:\"", "\",");
//			return apiKey;
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	});

	public TwitchTV() {
		ExecutorManager.get().queueTask(this.accquireClientKeyTask).addCallback((k) -> {
			TwitchTV.this.clientKey = k;
		});
	}

	public TwitchTV(final String key) {
		this.clientKey = key;
	}

	public static class ChannelStreamKeys {
		private final String token;
		private final String signature;
		private final String channel;

		public ChannelStreamKeys(final String channel, final String token, final String signature) {
			this.token = token;
			this.signature = signature;
			this.channel = channel;
		}
	}

	public static class ChannelURLRequest {
		public static enum RequestType {
			NORMAL,
			LOW_LATENCY,
			MOBILE,
		}

		private final ChannelStreamKeys keys;
		private final String channel;
		public RequestType type = RequestType.NORMAL;

		public ChannelURLRequest(final ChannelStreamKeys keys) {
			Variables.notNullE(keys);
			this.channel = keys.channel;
			this.keys = keys;
		}

		public ChannelURLRequest(final String channel, final ChannelStreamKeys keys) {
			Variables.notNullE(channel);
			Variables.notNullE(keys);
			this.channel = channel;
			this.keys = keys;
		}

		public ChannelStreamKeys getKeys() {
			return this.keys;
		}

		public RequestType getType() {
			return this.type;
		}
	}

	public ChannelStreamKeys generateToken(final String channel) throws Exception {
		return generateToken(channel, false);
	}

	public ChannelStreamKeys generateToken(final String channel, final boolean isMobile) throws Exception {
		if (this.clientKey == null && !this.accquireClientKeyTask.completed()) {
			this.clientKey = this.accquireClientKeyTask.waitTillCompletion();
		}
		if (channel == null || channel.isEmpty()) {
			throw new UnsupportedOperationException("The channel cannot be null or empty!");
		}
		final String oAuthToken = this.clientKey;
		String URL = null;
		if (!isMobile) {
			URL = "https://api.twitch.tv/api/channels/" + channel + "/access_token?need_https=true&oauth_token=" + oAuthToken
					+ "&platform=_&player_backend=mediaplayer&player_type=site&need_https=true";
		} /*
			 * else if (!USE_TTV_REWRITE_PATCHES && !isMobile) { URL =
			 * "https://api.twitch.tv/api/channels/" + channel +
			 * "/access_token?adblock=false&need_https=true&oauth_token=" + oAuthToken +
			 * "&platform=web&player_type=site"; }
			 */else {
			URL = "https://api.twitch.tv/api/channels/" + channel + "/access_token?adblock=false&need_https=true&oauth_token=" + oAuthToken
					+ "&platform=ios&player_type=iphone_t&need_https=true";
		}
		final HttpResponse<String> response = HttpClient.getString().url(URL).timeout(Duration.ofSeconds(10)).header("client-id", this.clientKey).create();
		if (response.status() == 200) {
			final UniJsonValue obj = UniJsonValue.parse(response.dataThenClose());
			final String sig = obj.getString("sig");
			final String token = obj.getString("token");
			return new ChannelStreamKeys(channel, token, sig);
		} else {
			throw new IOException("Got response: " + response.dataThenClose());
		}
	}

	public String getStreamM3U8(final ChannelURLRequest request) {
		Variables.notNullE(request);
		Variables.notNullE(request.type);
		Variables.notNullE(request.channel);
		if (request.channel.isEmpty())
			throw new IllegalArgumentException("channel is empty");
		String out = "";
		out += (PREFER_HTTPS ? "https://" : "http://");
		out += "usher.twitch.tv/api/channel/hls/" + request.channel + ".m3u8";
		out += "?token=" + LegacyURIUtils.safeEncode(request.keys.token);
		out += "&sig=" + request.keys.signature;
		out += "&allow_source=true";
		out += "&allow_audio_only=true";
		out += "&supported_codecs=avc1";
		out += "&cdm=wv";
		out += "&player_version=" + playerVersion;
		if (request.type == RequestType.NORMAL) {
//			if (USE_TTV_REWRITE_PATCHES) {
			out += "&player_backend=mediaplayer";
			out += "&playlist_include_framerate=true";
			out += "&reassignments_supported=true";
//			}else {
//				out += "&allow_source=true";
//				out += "&allow_audio_only=true";
			out += "&p=" + RandomUtil.randInt(10000, 99999);
//			}
		} else if (request.type == RequestType.LOW_LATENCY) {
			out += "&player_backend=mediaplayer";
			out += "&playlist_include_framerate=true";
			out += "&baking_bread=true";
			out += "&baking_brownies=true";
			out += "&baking_brownies_timeout=1050";
			out += "&fast_bread=true";
			// already patched when implemented :)
		} else if (request.type == RequestType.MOBILE) {
			out += "&platform=ios";
			out += "&player_type=iphone_t";
			out += "&allow_audio_only=true";
			out += "&allow_source=true";
			out += "&type=any";
//			if (USE_TTV_REWRITE_PATCHES) {
			out += "&player_backend=mediaplayer";
			out += "&playlist_include_framerate=true";
//			}
		}
		return out;
	}

	public String getKey() {
		return this.clientKey;
	}

	public long getChannelID(final String channel) {
		if (channel == null || channel.contains(" ") || channel.isEmpty()) {
			return -1;
		}
		if (this.clientKey == null && !this.accquireClientKeyTask.completed()) {
			this.clientKey = this.accquireClientKeyTask.waitTillCompletion();
		}
		if (CHANNEL_ID_MAPPINGS.containsKey(channel))
			return CHANNEL_ID_MAPPINGS.get(channel);
		try {
			final String URL = "https://api.twitch.tv/helix/search/channels?query=" + channel;
			final String resp = HttpClient.getString().url(URL).timeout(Duration.ofSeconds(10)).header("Client-ID", this.clientKey).create().dataThenClose();
			final UniJsonValue obj = UniJsonValue.parse(resp);
			if (!obj.hasValue("users")) {
				log.warn("Unknown response: " + obj);
				return -1;
			}
			final long id = obj.get("users").first().getLong("_id");
			CHANNEL_ID_MAPPINGS.put(channel, id);
			return id;
		} catch (Exception e) {
			log.error(e);
			return -1;
		}
	}

	public boolean isStreaming(final String channel) {
		if (channel == null || channel.contains(" ") || channel.isEmpty()) {
			return false;
		}
		if (this.clientKey == null && !this.accquireClientKeyTask.completed()) {
			this.clientKey = this.accquireClientKeyTask.waitTillCompletion();
		}
		try {
			final long channelID = getChannelID(channel);
			if (channelID == -1) {
				log.warn("isStreaming() -> channelID == -1!");
				return false;
			}
			final String URL = "https://api.twitch.tv/helix/search/channels?query=" + channelID;
			final String resp = HttpClient.getString().url(URL).timeout(Duration.ofSeconds(10)).header("Client-ID", this.clientKey).create().dataThenClose();
			final UniJsonValue obj = UniJsonValue.parse(resp);
			if (!obj.hasValue("stream"))
				return false;
			if (obj.hasValue("stream") && obj.get("stream") == null)
				return false;
			if (obj.hasValue("stream") && obj.get("stream").isNull())
				return false;
			if (obj.hasValue("stream") && obj.get("stream").toString().equalsIgnoreCase("null"))
				return false;
			return !obj.get("stream").toString().equals("null");
		} catch (Exception e) {
			log.error(e);
			return false;
		}
	}

	public static class StreamerMeta {
		private final boolean isStreaming;
		private volatile String currentGame;
		private final long viewers;
		private final String displayableString, minorString;
		private final String channelName, channel;

		protected StreamerMeta(final UniJsonValue jsonData, final String channelName) {
			this.channel = channelName;
			this.isStreaming = jsonData.hasValue("stream") && jsonData.get("stream") != null && !jsonData.get("stream").isNull()
					&& !jsonData.get("stream").toString().equalsIgnoreCase("null");
			if (this.isStreaming) {
				final UniJsonValue stream = jsonData.get("stream");
				try {
					this.currentGame = stream.getString("game");
				} catch (Exception e) {
					this.currentGame = "Unknown.";
					log.error("stream: " + stream.toString(), e);
					log.error(jsonData.toString());
				}
				this.viewers = stream.getLong("viewers", -1);
				if (stream.contains("channel")) {
					final UniJsonValue channel = stream.get("channel");
					this.displayableString = channel.getString("status");
					this.channelName = channel.getString("display_name");
					if (this.currentGame.equalsIgnoreCase("Just Chatting")) {
						this.minorString = this.channelName + " is " + this.currentGame + ".";
					} else {
						this.minorString = this.channelName + " is playing " + this.currentGame + ".";
					}
				} else {
					this.displayableString = null;
					this.channelName = channelName;
					this.minorString = null;
				}
			} else {
				this.currentGame = null;
				this.viewers = -1L;
				this.displayableString = null;
				this.channelName = channelName;
				this.minorString = null;
			}
		}

		public boolean isStreaming() {
			return this.isStreaming;
		}

		public long getViewers() {
			return this.viewers;
		}

		public String getGame() {
			return this.currentGame;
		}

		public boolean hasGame() {
			return this.currentGame != null;
		}

		public String getDisplayableString() {
			return this.displayableString;
		}

		public boolean hasDisplayableString() {
			return this.displayableString != null;
		}

		public UniJsonValue get() {
			final UniJsonValue obj = UniJsonValue.object();
			obj.add("streaming", this.isStreaming);
			obj.add("currentGame", this.currentGame);
			obj.add("viewers", this.viewers);
			obj.add("displayString", this.displayableString);
			obj.add("minorString", this.minorString);
			obj.add("channelName", this.channelName);
			obj.add("chatURL", this.getChatURL());
			obj.add("darkChatURL", this.getDarkChatURL());
			return obj;
		}

		public String getChannelName() {
			return this.channelName;
		}

		public String getMinorString() {
			return this.minorString;
		}

		public String getChatURL() {
			return "https://www.twitch.tv/embed/" + this.channel + "/chat";
		}

		public String getDarkChatURL() {
			return "https://www.twitch.tv/embed/" + this.channel + "/chat?darkpopout";
		}
	}

	public StreamerMeta accquire(final String channel) {
		if (channel == null || channel.contains(" ") || channel.isEmpty()) {
			return null;
		}
		if (this.clientKey == null && !this.accquireClientKeyTask.completed()) {
			this.clientKey = this.accquireClientKeyTask.waitTillCompletion();
		}
		try {
//			final long channelID = getChannelID(channel);
//			if (channelID == -1) throw new UnsupportedOperationException("Failed to get channel id!");
			final String URL = "https://gql.twitch.tv/gql";
			/*
			 * {"operationName":"PlaybackAccessToken","variables":{"isLive":true,"login":
			 * "zerkaa","isVod":false,"vodID":"","playerType":"frontpage"},"extensions":{
			 * "persistedQuery":{"version":1,"sha256Hash":
			 * "0828119ded1c13477966434e15800ff57ddacf13ba1911c129dc2200705b0712"}}}
			 */
			JVM.println("clientID: " + this.clientKey);
			final UniJsonValue o = UniJsonValue.object();
			o.add("operationName", "PlaybackAccessToken");
			final UniJsonValue exts = UniJsonValue.object();
			final UniJsonValue pq = UniJsonValue.object();
			pq.add("version", 1);
			pq.add("sha256Hash", "0828119ded1c13477966434e15800ff57ddacf13ba1911c129dc2200705b0712");
			exts.add("persistedQuery", pq);
			o.add("extensions", exts);
			final UniJsonValue vars = UniJsonValue.object();
			vars.add("playerType", "frontpage");
			vars.add("login", channel);
			vars.add("vodID", "");
			vars.add("variables", vars);
			vars.add("isLive", true);
			vars.add("isVod", false);
			o.add("variables", vars);
			JVM.println(o.toString());
			final String resp = HttpClient.getString().url(URL).header("Client-ID", this.clientKey).create(HttpMethod.POST, o.toString()).dataThenClose();
			final UniJsonValue obj = UniJsonValue.parse(resp);
			obj.println();
			return new StreamerMeta(obj, channel);
		} catch (Exception e) {
			JVM.errorln(e);
			return null;
		}
	}

	public static void main(String[] args) throws Exception {
		final TwitchTV ttv = new TwitchTV();
		ttv.accquireClientKeyTask.waitTillCompletion();
		final String channel = "forsen";
//		final ChannelStreamKeys r = ttv.generateToken(channel);
//		JVM.println("signature: " + r.signature);
//		JVM.println("token: " + r.token);
//		final ChannelURLRequest u = new ChannelURLRequest(r);
//		JVM.println("url: " + ttv.getStreamM3U8(u));
//		JVM.println("isStreaming: " + ttv.isStreaming(channel));
//		JVM.println(ttv.accquire(channel).isStreaming);
		JVM.println(ttv.accquire(channel));
	}
}
